/*
   Copyright 2009 Abdulla Abdurakhmanov (abdulla@latestbit.com)
   Original sources are available at www.latestbit.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.IO;
using org.lineargs.constraints;

namespace org.lineargs
{	
	/// <summary> Command line arguments parser implementation</summary>
	public class LineArgsParser
	{		
		/// <summary> Constructor</summary>
		public LineArgsParser()
		{
		}
		
		/// <summary>Do parse arguments</summary>
		/// <param name="objClass">An object class which contains of annotated properties.</param>
		/// <param name="args">Command line arguments</param>
		/// <returns> Object which contains a filled properties</returns>
        public virtual T parse<T>(string[] args) 
        {
            T obj  = (T)Activator.CreateInstance(typeof(T));
            parse(obj, args);
            return obj;
        }

        /// <summary>Do parse arguments</summary>
        /// <param name="obj">An object which contains of annotated properties.</param>
        /// <param name="args">Command line arguments</param>
        /// <returns> Object which contains a filled properties</returns>
        public virtual T parse<T>(T obj, string[] args)
        {
            parseArguments(obj, args);            
            return obj;
        }

       protected virtual void parseArguments(Object obj, string[] args)
       {
            ICollection<PropertyInfo> installedFields = new LinkedList<PropertyInfo>();
            int currentArg = 0;        
            while(currentArg<args.Length) {
                PropertyInfo[] fields = obj.GetType().GetProperties();
                int idx=0; 
                 for(idx=0; idx<fields.Length;idx++) {
                    PropertyInfo field = fields[idx];
                    String arg = args[currentArg];
                    String name = getFieldName(field);
                    String shortName = getFieldShortName(field);
                    if( !installedFields.Contains(field) &&
                        ((name!=null && arg.Equals(name)) || ( shortName!=null && shortName.Length>0 && arg.Equals(shortName)))) {
                        currentArg = parseArgument(obj,field,args,currentArg);
                        //currentArg++;
                        installedFields.Add(field);
                        break;
                    }
                    else
                    if(isAttributePresent(field,typeof(NonameOption)) && !installedFields.Contains(field) ) {
                            currentArg = parseNoname(obj,field,args,currentArg);
                            //currentArg++;
                            installedFields.Add(field);
                            break;
                    }
                }
                if(idx == fields.Length) 
                    throw new ApplicationException ("Unknown option: " + args[currentArg]);
                
                currentArg++;
            }
            checkMandatoryFields(obj,installedFields);
        }

        protected bool isAttributePresent(PropertyInfo field, Type attrType) 
        {
            object[] attrs = field.GetCustomAttributes(attrType,false);
            if (attrs != null && attrs.Length > 0)
                return true;
            else
                return false;
        }

        protected T getAttribute<T>(PropertyInfo field)
        {
            object[] attrs = field.GetCustomAttributes(typeof(T), false);
            if (attrs != null && attrs.Length > 0)
            {
                T attribute = (T)attrs[0];
                return attribute;
            }
            else
                return default(T);
        }

        protected virtual int parseArgument(Object obj, PropertyInfo field, string[] args, int currentArg)
        {
            if (isAttributePresent(field,typeof(ListOption)))
            {
                currentArg = parseSeparated(obj, field, args, currentArg);
            }
            else
            if (isAttributePresent(field,typeof(Option)))
            {
                currentArg = parseOption(obj, field, args, currentArg);
            }
            return currentArg;
        }

        protected virtual int parseOption(Object obj, PropertyInfo field, string[] args, int currentArg)
        {
            Option meta = getAttribute<Option>(field);
            Type objClass = field.PropertyType;
            Object val = null;
            if (!objClass.Equals(typeof(bool)))
            {
                if (++currentArg >= args.Length)
                {
                    throw new ApplicationException("Value for option " + meta.Name + " is unspecified!");
                }

                //Object val = Activator.CreateInstance(objClass);
                val = initPropertyWithValue(objClass, args[currentArg]);
            }
            else
            {
                val = true;
            }
            checkConstraints(meta.Name, val, field);
            setFieldValue(obj, field, val);
            return currentArg;
        }

        protected Object initPropertyWithValue(Type objClass, String arg)
        {
            Object val = null;
            if (objClass.Equals(typeof(string)))
            {
                val = arg;
            }
            else
            if (objClass.Equals(typeof(int)))
            {
                val = System.Int32.Parse(arg);
            }
            /*else
            if (objClass.Equals(typeof(bool)))
            {
                val = System.Boolean.Parse(arg);
            }*/
            return val;
        }

        protected virtual void checkConstraints(string optionName, Object obj, PropertyInfo field)
        {
            if (obj is int)
            {
                int val = (int)obj;
                if (isAttributePresent(field,typeof(RangeConstraint)))
                {
                    RangeConstraint constraint = getAttribute<RangeConstraint>(field);
                    if (val > constraint.MaxValue || val < constraint.MinValue)
                        throw new ApplicationException("Specified value for option " + optionName + " is incorrect!");
                }
            }
            else
            {
                if (isAttributePresent(field,typeof(RangeConstraint)))
                {
                    string strValue = obj.ToString();
                    RangeConstraint constraint = getAttribute<RangeConstraint>(field);
                    if (strValue.Length > constraint.MaxValue || strValue.Length < constraint.MinValue)
                        throw new ApplicationException("Specified value for option " + optionName + " has incorrect length!");
                }
            }
            if (isAttributePresent(field,typeof(RegexConstraint)))
            {
                string strValue = obj.ToString();
                RegexConstraint constraint = getAttribute<RegexConstraint>(field);
                Regex rx = new Regex(constraint.Mask);
                Match m = rx.Match(strValue);
                if (!m.Success)
                    throw new ApplicationException("Specified value for option " + optionName + " is incorrect!");
            }
        }
        
        protected virtual int parseSeparated(Object obj, PropertyInfo field, string[] args, int currentArg)
        {
            ListOption meta = getAttribute<ListOption>(field);
            string arg = args[currentArg];
            if (++currentArg >= args.Length)
            {
                throw new ApplicationException("Value for option " + meta.Name + " is unspecified!");
            }
            string val = args[currentArg];
            string[] tokens = val.Split(new Char[] { meta.Delimeter });
            Type paramType = (System.Type) field.PropertyType.GetGenericArguments()[0];
            //ICollection < Object > param = new LinkedList < Object >();
            Type collectionType = typeof(List<>);
            Type genCollectionType = collectionType.MakeGenericType(paramType);
            //Object param = Activator.CreateInstance(field.PropertyType);
            Object param = Activator.CreateInstance(genCollectionType);
            foreach(string token in tokens)
            {
                Object oVal = initPropertyWithValue(paramType, token);
                checkConstraints(meta.Name, oVal, field);
                MethodInfo method = param.GetType().GetMethod("Add");
                method.Invoke(param, new object[] { oVal });
            }
            setFieldValue(obj, field, param);
            return currentArg;
        }

	
        protected virtual int parseNoname(Object obj, PropertyInfo field, string[] args, int currentArg)
        {
            Option meta = getAttribute<Option>(field);
            // BugFix 1.0.0b2. NonameOption option does not have any next arguments.
            /*if (++currentArg >= args.Length)
            {
                throw new ApplicationException("Value is not specified!");
            }*/
            Type objClass = field.PropertyType;
            //Object val = Activator.CreateInstance(objClass, args[currentArg]);
            Object val = initPropertyWithValue(objClass, args[currentArg]);
            checkConstraints("<value>", val, field);
            setFieldValue(obj, field, val);
            return currentArg;
        }

        protected virtual void  checkMandatoryFields(Object obj, ICollection<PropertyInfo> installedFields)
        {
            foreach(PropertyInfo field in obj.GetType().GetProperties())
            {
                if (!isFieldOptional(field) && !installedFields.Contains(field))
                {
                    if (isAttributePresent(field, typeof(NonameOption)))
                    {
                        throw new ApplicationException("Unable to present mandatory argument (" + getFieldDescription(field)+")");
                    }
                    else
                        throw new ApplicationException("Unable to present value for mandatory option " + getFieldName(field));
                }
            }
        }
		
        protected virtual bool isFieldOptional(PropertyInfo field)
        {
            if (isAttributePresent(field,typeof(Option)))
            {
                return getAttribute<Option>(field).IsOptional;
            }
            else
            if (isAttributePresent(field, typeof(NonameOption)))
            {
                return getAttribute<NonameOption>(field).IsOptional;
            }
            else 
            if (isAttributePresent(field,typeof(ListOption)))
            {
                return getAttribute<ListOption>(field).IsOptional;
            }
            else
                return true;
        }
		
        protected virtual string getFieldDescription(PropertyInfo field)
        {
            if (isAttributePresent(field,typeof(Option)))
            {
                return getAttribute<Option>(field).Description;
            }
            else
            if (isAttributePresent(field, typeof(NonameOption)))
            {
                return getAttribute<NonameOption>(field).Description;
            }
            else 
            if (isAttributePresent(field,typeof(ListOption)))
            {
                return getAttribute<ListOption>(field).Description;
            }
            else
                return null;
        }

        protected virtual string getFieldName(PropertyInfo field)
        {
            if (isAttributePresent(field, typeof(Option)))
            {
                return getAttribute<Option>(field).Name;
            }
            else
            if (isAttributePresent(field, typeof(ListOption)))
            {
                return getAttribute<ListOption>(field).Name;
            }
            else
                return null;
        }

        protected virtual string getFieldShortName(PropertyInfo field)
        {
            if (isAttributePresent(field, typeof(Option)))
            {
                return getAttribute<Option>(field).ShortName;
            }
            else
            if (isAttributePresent(field, typeof(ListOption)))
            {
                return getAttribute<ListOption>(field).ShortName;
            }
            else
                return null;
        }

        protected virtual Object getFieldValue(Object obj, PropertyInfo field)
        {
            return field.GetValue(obj,null);
        }

        protected virtual void  setFieldValue(Object obj, PropertyInfo field, Object param)
        {
            field.SetValue(obj, param, null);
        }
			
        /// <summary> Print to stream help for specified options
        /// <param name="objClass">An object which contains of annotated properties. 
        /// </param>
        /// <param name="stream">Stream to write help
        /// </param>
        public void printHelp<T>(TextWriter stream)
        {
            Type objClass = typeof(T);
            foreach(PropertyInfo field  in objClass.GetProperties()) {
                string fieldHelp = getHelpForField(field);
                if(fieldHelp!=null) {
                    string val = fieldHelp;
                    stream.WriteLine(val);
                }
            }
        }

        protected virtual string getHelpForField(PropertyInfo field)
        {
            string name = getFieldName(field), shortName = getFieldShortName(field);
            if (name == null)
            {
                if (!isAttributePresent(field,typeof(NonameOption)))
                    return null;
                else
                    name = "";
            }
            bool isOptional = isFieldOptional(field);
            bool hasArgument = !field.PropertyType.Equals(typeof(Boolean));
            
            if(isAttributePresent(field,typeof(NonameOption))) 
            {
                return String.Format(
                    "{0,36:s}","" + (isOptional ? "[" : "") + "<value>" + (isOptional ? "[" : "") + " : ")
                    + getFieldDescription(field);
            }
            else
            {
                return String.Format(
                    "{0,18:s} {1,8:s} {2,6:s}: {3,0:s}"
                    , new Object[] { (
                        isOptional ? "[" : "") + name+",", 
                        shortName + (isOptional ? "]":""), 
                        (hasArgument ? " <arg>": ""),
                        getFieldDescription(field) 
                    }
                );
            }
        }
    }
}